import os
import random
from abc import ABCMeta, abstractmethod
from typing import Optional, List, Tuple
from sqlalchemy.orm import Session
from docker import DockerClient
from docker.models.networks import Network
from docker.models.services import ServiceMode
from docker.types import EndpointSpec, Resources
from app.controller.model.storage import StorageTypeEnum, StorageScaleEnum
from app.dao.model.storage import Storage, Port
from app.docker import Client
from app.dao.storage import create_storage
from app.utils.rand import get_new_rand_str


def get_new_name(client: DockerClient, prefix: str = ''):
    networks = client.networks.list()
    network_names = [item.name for item in networks]

    while True:
        name = get_new_rand_str()
        if f'{prefix}{name}' not in network_names:
            return name


def get_free_port():
    while True:
        res = 10000+random.randint(1, 3000)
        if os.popen('netstat -na | grep :' + str(res)).readlines():
            continue

        return res


def commit_storage(db: Session, *, service_id: str,
                   main_memory: int,
                   slave_memory: int = None,
                   type: int = 3,
                   ports: List[Tuple[int, int]] = []):
    tmp = []
    for port in ports:
        tmp.append(Port(host_port=port[0], container_port=port[1]))

    create_storage(
        db,
        storage=Storage(
            service_id=service_id,
            main_memory=main_memory,
            slave_memory=slave_memory,
            type=type,
            ports=tmp
        )
    )


class StorageCreator(metaclass=ABCMeta):
    @abstractmethod
    def create(self) -> str:
        pass


class MySQLCreator(StorageCreator):
    prefix = 'MySQL'
    image = 'mysql:5.7.37'

    db: Session
    storage_scale: StorageScaleEnum
    client: DockerClient
    main_node_memory = 128
    slave_node_memory = 128
    rand_str = ''
    port: int

    network: Network
    env: List[str]
    mode: ServiceMode
    endpoint: EndpointSpec
    resource: Resources

    def __init__(self, db: Session, storage_scale: StorageScaleEnum, main_node_memory: int = 128, *, client: DockerClient) -> None:
        super().__init__()
        self.db = db
        self.storage_scale = storage_scale
        self.main_node_memory = main_node_memory
        self.client = client

        self.rand_str = get_new_name(self.client, self.prefix)
        self.port = get_free_port()

    def _create_network(self):
        self.network = self.client.networks.create(
            f'{self.prefix}{self.rand_str}',
            driver="overlay"
        )

    def _create_env(self):
        self.env = ["MYSQL_ROOT_PASSWORD=123456"]

    def _create_mode(self):
        self.mode = ServiceMode('replicated', self.storage_scale.value)

    def _create_endpoint(self):
        self.endpoint = EndpointSpec(ports=dict([(self.port, 3306)]))

    def _create_resource(self):
        self.resource = Resources(mem_limit=self.main_node_memory*1024*1024)

    def create(self) -> str:
        self._create_network()
        self._create_env()
        self._create_mode()
        self._create_endpoint()
        self._create_resource()

        service = self.client.services.create(
            image=self.image,
            networks=[self.network.id],
            name=f'{self.prefix}{self.rand_str}',
            mode=self.mode,
            env=self.env,
            endpoint_spec=self.endpoint
        )

        commit_storage(
            self.db,
            service_id=service.id,
            main_memory=self.main_node_memory,
            ports=[(self.port, 3306)],
            type=3
        )

        return service.id


class HDFSCreator(StorageCreator):
    prefix = 'HDFS'
    image = 'test_hadoop:1.8'
    commandNameNode = 'namenode.sh'
    commandDataNode = 'datanode.sh'

    db: Session
    storage_scale: StorageScaleEnum
    client: DockerClient
    main_node_memory = 128
    slave_node_memory = 128
    rand_str = ''
    port_9000 = 9000
    port_9870 = 9870
    namenode_ip: str
    namenode_name: str

    network: Network
    endpoint: EndpointSpec

    def __init__(self, db: Session, storage_scale: StorageScaleEnum, main_node_memory: int = 128, slave_node_memory: int = 128, *, client: DockerClient) -> None:
        super().__init__()
        self.db = db
        self.storage_scale = storage_scale
        self.main_node_memory = main_node_memory
        self.slave_node_memory = slave_node_memory
        self.client = client

        self.rand_str = get_new_name(self.client, self.prefix)
        self._create_twoports()

    def _create_twoports(self):
        self.port_9000 = get_free_port()
        while True:
            self.port_9870 = get_free_port()
            if self.port_9870 != self.port_9000:
                break

    def _create_network(self):
        self.network = self.client.networks.create(
            f'{self.prefix}{self.rand_str}',
            driver="overlay"
        )

    def _create_endpoint(self):
        self.endpoint = EndpointSpec(
            ports=dict([(self.port_9000, 9000), (self.port_9870, 9870)])
        )

    def _get_node_addr(self, main_node_id):
        ins = self.client.api.inspect_node(main_node_id)
        return ins['Status']['Addr']

    def _create_namenode(self):
        service = self.client.services.create(
            image=self.image,
            command=["/bin/bash", self.commandNameNode],
            hostname="namenode",
            networks=[self.network.id],
            name=f'{self.prefix}{self.rand_str}',
            endpoint_spec=self.endpoint
        )

        return service

    def _create_datanode(self, index: int):
        self.client.services.create(
            image=self.image,
            command=["/bin/bash", self.commandDataNode],
            networks=[self.network.id],
            name=f'{self.prefix}{self.rand_str}_dn{index}',
        )

    def create(self) -> str:
        self._create_network()
        self._create_endpoint()
        service = self._create_namenode()

        for i in range(self.storage_scale.value):
            self._create_datanode(i)

        commit_storage(
            self.db,
            service_id=service.id,
            main_memory=self.main_node_memory,
            slave_memory=self.slave_node_memory,
            ports=[(self.port_9000, 9000), (self.port_9870, 9870)],
            type=1
        )
        return service.id


class HBaseCreator(StorageCreator):
    prefix = 'HBase'
    image = 'harisekhon/hbase:latest'

    db: Session
    storage_scale: StorageScaleEnum
    client: DockerClient
    main_node_memory = 128
    slave_node_memory = 128
    rand_str = ''
    port: int

    network: Network
    endpoint: EndpointSpec

    def __init__(self, db: Session, storage_scale: StorageScaleEnum, main_node_memory: int = 128, slave_node_memory: int = 128, *, client: DockerClient) -> None:
        super().__init__()
        self.db = db
        self.storage_scale = storage_scale
        self.main_node_memory = main_node_memory
        self.slave_node_memory = slave_node_memory
        self.client = client

        self.rand_str = get_new_name(self.client, self.prefix)
        self.port = get_free_port()

    def _create_network(self):
        self.network = self.client.networks.create(
            f'{self.prefix}{self.rand_str}',
            driver="overlay"
        )

    def _create_endpoint(self):
        self.endpoint = EndpointSpec(ports=dict([(self.port, 16010)]))

    def create(self) -> str:
        self._create_network()
        self._create_endpoint()

        service = self.client.services.create(
            image=self.image,
            networks=[self.network.id],
            name=f'{self.prefix}{self.rand_str}',
            endpoint_spec=self.endpoint
        )

        commit_storage(
            self.db,
            service_id=service.id,
            main_memory=self.main_node_memory,
            slave_memory=self.slave_node_memory,
            ports=[(self.port, 16010)],
            type=2
        )

        return service.id


def get_storage_creator(
        db: Session,
        storage_type: StorageTypeEnum,
        *,
        storage_scale: StorageScaleEnum,
        main_node_memory: int = 128,
        slave_node_memory: int = 128) -> Optional[StorageCreator]:
    if storage_type is StorageTypeEnum.MySQL:
        return MySQLCreator(db, storage_scale, main_node_memory, client=Client().client)
    elif storage_type is StorageTypeEnum.HDFS:
        return HDFSCreator(db, storage_scale, main_node_memory, slave_node_memory, client=Client().client)
    elif storage_type is StorageTypeEnum.HBase:
        return HBaseCreator(db, storage_scale, main_node_memory, slave_node_memory, client=Client().client)
    return None
